Current File : /home/jeconsul/public_html/wp-content/plugins/suremails/inc/emails/handler/mail-handler.php
<?php
/**
 * MailHandler.php
 *
 * Handles sending emails using different connections and logging the activities.
 *
 * @package SureMails\Inc\Emails\Handler
 */

namespace SureMails\Inc\Emails\Handler;

use SureMails\Inc\Admin\Crons;
use SureMails\Inc\ConnectionManager;
use SureMails\Inc\Controller\ContentGuard;
use SureMails\Inc\Controller\Logger;
use SureMails\Inc\Emails\DefaultMailHandler;
use SureMails\Inc\Utils\LogError;
use WP_Error;

/**
 * Class MailHandler
 *
 * Handles sending emails using different connections and logging the activities.
 */
class MailHandler {

	/**
	 * Singleton instance.
	 *
	 * @var MailHandler|null
	 */
	private static $instance = null;

	/**
	 * Logger instance.
	 *
	 * @var Logger
	 */
	private $logger;

	/**
	 * ConnectionManager instance.
	 *
	 * @var ConnectionManager
	 */
	private $connection_manager;

	/**
	 * ProcessEmailData instance.
	 *
	 * @var ProcessEmailData
	 */
	private $email_data_processor;

	/**
	 * ContentGuard instance.
	 *
	 * @var ContentGuard
	 */
	private $content_guard;
	/**
	 * Private constructor to enforce Singleton pattern.
	 */
	private function __construct() {
		$this->logger               = Logger::instance();
		$this->connection_manager   = ConnectionManager::instance();
		$this->email_data_processor = ProcessEmailData::instance();
		$this->content_guard        = ContentGuard::instance();
		add_filter( 'suremails_before_send_email', [ $this->content_guard, 'check_email_content' ], 10 );
	}

	/**
	 * Retrieves the Singleton instance of MailHandler.
	 *
	 * @return MailHandler The Singleton instance.
	 */
	public static function get_instance(): MailHandler {
		if ( self::$instance === null ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Handles sending an email based on the provided attributes.
	 *
	 * @param array $atts The email attributes.
	 * @return bool|null The result of the email sending process.
	 */
	public static function handle_mail( array $atts ) {
		return self::get_instance()->process_mail( $atts );
	}

	/**
	 * Processes the email sending logic.
	 *
	 * @param array $atts The email attributes.
	 * @return bool The result of the email sending process.
	 */
	private function process_mail( array $atts ) {

		// Apply pre_wp_mail filter.
		$pre_wp_mail = apply_filters( 'pre_wp_mail', null, $atts );
		if ( $pre_wp_mail !== null ) {
			return $pre_wp_mail;
		}

		$mail_data = [
			'to'          => $atts['to'],
			'subject'     => $atts['subject'],
			'message'     => $atts['message'],
			'headers'     => $atts['headers'],
			'attachments' => $atts['attachments'],
		];

		$result = apply_filters( 'suremails_before_send_email', $atts );

		if ( is_array( $result ) ) {
			$mail_data['categories'] = $result;
			do_action( 'suremails_mail_blocked', $mail_data );
			return false;
		}

		// Get the globally shared PHPMailer instance.
		$phpmailer = $this->connection_manager->get_phpmailer();

		$processed_data = $this->process_email_data( $atts );

		$connection = $this->connection_manager->get_connection();
		if ( $connection === null ) {
			$connection = $this->determine_connection( $processed_data['headers'] );
		}
		$connection = $this->review_email_settings( $connection, $processed_data['headers']['from'] );

		// Initialize handler_response.
		$handler_response = [
			'atts'             => $processed_data,
			'status'           => Logger::STATUS_PENDING,
			'message'          => '',
			'success'          => false,
			'source'           => $connection['type'] ?? 'Default',
			'connection_title' => $connection['connection_title'] ?? 'Default',
			'from'             => [
				'email' => $connection['from_email'] ?? '',
				'name'  => ! empty( $connection['from_name'] ) ? $connection['from_name'] : __( 'WordPress', 'suremails' ),
			],
		];

		if ( $connection === null ) {
			// Send via DefaultMailHandler.
			$send = DefaultMailHandler::send_mail( $atts );
			if ( $send ) {
				$handler_response['status']  = Logger::STATUS_SENT;
				$handler_response['message'] = 'Sent using Default WordPress Handler';
				$handler_response['success'] = true;
			} else {
				$handler_response['message'] = 'Failed to send email using Default WordPress Handler';
				$handler_response['success'] = false;

			}

			$this->handle_response( $handler_response );
			return $send;
		}
		// Use handler.
		$handler = ConnectionHandlerFactory::create( $connection );
		if ( ! $handler instanceof ConnectionHandler ) {
			$handler_response['message'] = 'Invalid connection type.';
			$this->handle_response( $handler_response );
			return false;
		}

		if ( ! apply_filters( 'suremails_send_email', '__return_true' ) ) {
			return false;
		}

		// Send via handler.
		$send_result = $handler->send( $atts, $this->logger->get_id(), $connection, $processed_data );

		// Setting status and messageee.
		$handler_response['success']         = $send_result['success'] ?? false;
		$handler_response['status']          = $handler_response['success'] ? Logger::STATUS_SENT : Logger::STATUS_FAILED;
		$handler_response['message']         = $send_result['message'] ?? ( $handler_response['success'] ? __( 'Email sent successfully.', 'suremails' ) : __( 'Failed to send email.', 'suremails' ) );
		$handler_response['email_simulated'] = $send_result['email_simulated'] ?? false;

		if ( $handler_response['success'] ) {
			do_action( 'wp_mail_succeeded', $mail_data );
			$this->handle_response( $handler_response );
			$this->connection_manager->reset();
			$this->logger->set_id();
			return true;
		}

		// After Failed Actions:.
		$mail_error_data                             = $mail_data;
		$mail_error_data['phpmailer_exception_code'] = 0;

		// Log the result.
		$log_id = $this->handle_response( $handler_response );

		// Attempt fallback if not in testing mode.
		if ( ! $this->connection_manager->get_is_testing() ) {
			$next_connection = $this->connection_manager->get_next_connection();
			if ( $next_connection !== null ) {
				$this->logger->set_id( $log_id );
				$this->connection_manager->set_connection( $next_connection );
				$send_result_fallback = self::handle_mail( $atts );
				if ( $send_result_fallback ) {
					$this->connection_manager->reset();
					return true;
				}
			}
		}
		if ( $this->should_trigger_failed_email() && $handler_response['status'] === Logger::STATUS_FAILED ) {
			do_action( 'wp_mail_failed', new WP_Error( 'wp_mail_failed', $handler_response['message'], $mail_data ) );
		}

		$resend_log_id = $this->logger->get_id();
		if (
			is_int( $resend_log_id )
			&& $handler_response['status'] === Logger::STATUS_FAILED &&
			! $this->connection_manager->get_is_testing()
		) {
			Crons::instance()->schedule_retry_failed_email( $resend_log_id );
		}
		$this->connection_manager->reset();
		return false;
	}

	/**
	 * Processes the email data using ProcessEmailData class and populates PHPMailer.
	 *
	 * @param array $atts The email attributes.
	 * @return array Processed email data.
	 */
	private function process_email_data( array $atts ) {
		$to          = $atts['to'] ?? [];
		$headers     = $atts['headers'] ?? [];
		$message     = $atts['message'] ?? '';
		$attachments = $atts['attachments'] ?? [];
		$subject     = $atts['subject'] ?? '';

		$processed_data = $this->email_data_processor->process_all( $to, $headers, $message, $attachments, $subject );

		// Update $atts with processed data.
		$atts['to']          = $processed_data['to'];
		$atts['headers']     = $this->email_data_processor->format_processed_headers( $processed_data['headers'] );
		$atts['message']     = $processed_data['message'];
		$atts['attachments'] = $processed_data['attachments'];
		$atts['subject']     = $processed_data['subject'];

		return $processed_data;
	}

	/**
	 * Handles the response from email sending and logging.
	 *
	 * @param array $handler_response The response data from the handler.
	 * @return int|null The log ID after handling the response.
	 */
	private function handle_response( array $handler_response ) {

		$new_server_response = [
			'retry'      => 0,
			'Message'    => $handler_response['message'],
			'Connection' => $handler_response['connection_title'],
			'timestamp'  => current_time( 'mysql' ),
			'simulated'  => $handler_response['email_simulated'] ?? false,

		];

		$atts              = $handler_response['atts'];
		$status            = $handler_response['status'];
		$source            = $handler_response['source'];
		$from_email        = $handler_response['from']['email'];
		$from_name         = $handler_response['from']['name'];
		$email_from        = "{$from_name} <{$from_email}>";
		$email_to          = $this->email_data_processor->format_email_recipients( $atts['to'] );
		$formatted_headers = $this->email_data_processor->format_processed_headers( $atts['headers'] );

		// Prepare log data.
		$log_data = $this->logger->prepare_log_data(
			[
				'email_from'  => $email_from,
				'email_to'    => $email_to,
				'subject'     => $atts['subject'] ?? '',
				'body'        => $atts['message'] ?? '',
				'headers'     => $formatted_headers,
				'attachments' => $atts['uploaded_attachments'] ?? [],
				'status'      => $status,
				'response'    => [ $new_server_response ],
				'connection'  => $source,
			]
		);

		if ( $log_data['status'] !== Logger::STATUS_SENT && ! $this->connection_manager->get_is_testing() && ! $this->connection_manager->get_is_resend() ) {
			$log_data['status'] = Logger::STATUS_PENDING;
		}

		// Check if log_id is already set.
		$log_id = $this->logger->get_id();

		if ( $log_id === null ) {

			// First time logging.
			$log_id = $this->logger->log_email( $log_data );

			if ( is_wp_error( $log_id ) ) {
				LogError::instance()->log_error( 'Failed to log email: ' . $log_id->get_error_message() );
				return null;
			}
			if ( is_int( $log_id ) && $log_data['status'] === Logger::STATUS_PENDING ) {
				$this->logger->set_id( $log_id );
				return $log_id;
			}
			return null;
		}

		// Update existing log.
		$log_entry = (array) $this->logger->get_log( $log_id );
		$meta      = $log_entry['meta'] ?? [
			'retry'  => 0,
			'resend' => 0,
		];

		if ( $this->should_retry_increase() ) {
			$meta['retry'] = (int) $meta['retry'] + 1;
		}

		if ( $log_data['status'] === Logger::STATUS_PENDING && $meta['retry'] >= 1 ) {
			$log_data['status'] = Logger::STATUS_FAILED;
		}
		$new_server_response['retry'] = $meta['retry'];

		if ( $this->connection_manager->get_is_resend() && $log_data['status'] === Logger::STATUS_SENT ) {
			$meta['resend'] += 1;
		}

		$existing_responses = $log_entry['response'];
		if ( ! is_array( $existing_responses ) ) {
			$existing_responses = [];
		}

		$existing_responses[] = $new_server_response;

		$update_data = [
			'status'     => $log_data['status'],
			'response'   => $existing_responses,
			'updated_at' => current_time( 'mysql' ),
			'connection' => $source,
			'meta'       => $meta,
			'email_from' => $log_data['email_from'],
		];

		$update_result = $this->logger->update_log( $log_id, $update_data );
		if ( is_wp_error( $update_result ) || ! $update_result ) {
			// Handle update failure if necessary.
			LogError::instance()->log_error( "Failed to update log ID {$log_id}." );
			return null;
		}

		do_action( 'suremails_after_send_mail', $log_data, $log_id );

		// Return the $log_id for further use.
		return $log_id;
	}

	/**
	 * Determines which connection to use based on email attributes.
	 *
	 * @param array $headers The email headers.
	 * @return array|null The connection details or null if not found.
	 */
	private function determine_connection( array $headers ) {
		$from       = $headers['from'] ?? null;
		$from_name  = ! empty( $from['name'] ) ? $from['name'] : null;
		$from_email = ! empty( $from['email'] ) ? $from['email'] : null;

		$connection = null;

		if ( $from_email !== null ) {
			$connection = $this->get_connection_from_email( $from_email );
		}

		if ( $connection === null ) {
			// No connection found for the from_email. Use the default connection first then get all connections from from_email of default connection and use as fallback sequence based on priority.
			$default_connection = $this->connection_manager->get_default_connection( false );
			$default_from_email = $default_connection['from_email'] ?? null;
			$this->connection_manager->set_from_email( $default_from_email );

			// Swap default connection to true to get all connections from from_email of default connection but first connection should be default connection.
			$this->connection_manager->swap_default_connection = true;
			$connection                                        = $this->get_connection_from_email( $default_from_email );
		}

		return $connection;
	}

	/**
	 * Get connection details based on from_email and priority.
	 *
	 * @param string $from_email The email address to match.
	 * @return array|null The connection details with the highest priority or null if not found.
	 */
	private function get_connection_from_email( string $from_email ) {
		$best_connection = null;

		if ( $this->connection_manager->get_from_email() ) {
			$best_connection = $this->connection_manager->get_next_connection();
		}
		$this->connection_manager->set_from_email( $from_email );
		return $this->connection_manager->get_priority_based_fallback_connection();
	}

	/**
	 * Determines whether to increase the retry count.
	 *
	 * @return bool
	 */
	private function should_retry_increase() {
		return (
			! $this->connection_manager->get_is_resend() &&
			$this->connection_manager->get_is_retried() &&
			$this->connection_manager->get_is_first()
		) || (
			$this->connection_manager->is_default &&
			$this->connection_manager->get_is_retried()
		);
	}

	/**
	 * Determines whether to trigger the failed email action.
	 * This trigger will only trigger if the email is retried or testing and failed and it's last connection or default connection.
	 *
	 * @return bool Whether to trigger the failed email action.
	 */
	private function should_trigger_failed_email() {
		if ( $this->connection_manager->get_is_testing() ) {
			return true;
		}

		if ( $this->connection_manager->get_is_retried() && $this->connection_manager->is_default ) {
			return true;
		}

		if ( $this->connection_manager->get_is_retried() && $this->connection_manager->get_is_last() ) {
			return true;
		}

		if ( $this->connection_manager->get_is_resend() && $this->connection_manager->get_is_last() ) {
			return true;
		}

		return false;
	}

	/**
	 * Reviews and updates the connection's email settings based on the given $from and force settings.
	 *
	 * @param array|null $connection The connection details (associative array).
	 * @param array|null $from       The "from" details, typically containing 'name' and 'email'.
	 * @param string     $default_name    The default name to use if none is provided and force is false (e.g., 'WordPress').
	 * @return array|null            The updated connection or null if the connection is null.
	 */
	private function review_email_settings( ?array $connection, ?array $from, string $default_name = '' ) {
		if ( $connection === null ) {
			return null;
		}

		$default_name = ! empty( $default_name ) ? $default_name : __( 'WordPress', 'suremails' );

		if ( isset( $connection['force_from_name'] ) ) {
			if ( $connection['force_from_name'] === false ) {
				if ( ! empty( $from['name'] ) ) {
					$connection['from_name'] = $from['name'];
				} elseif ( empty( $connection['from_name'] ) ) {

					$connection['from_name'] = $default_name;
				}
			}
		}

		if ( isset( $connection['force_from_email'] ) ) {
			if ( $connection['force_from_email'] === false ) {
				if ( ! empty( $from['email'] ) ) {
					$connection['from_email'] = $from['email'];
				}
			}
		}

		$phpmailer = $this->connection_manager->get_phpmailer();
		$phpmailer->setFrom( $connection['from_email'], $connection['from_name'] );

		return $connection;
	}

}